What is Object-Oriented Programming?

Programming oriented around objects

"object" : A structure with both data and functions

Similar to a C or IDL struct, but with functions "bound" to them.

How do you use an object?

In [1]:
fobj = open('../README.md')
In [2]:
fobj.name
Out[2]:
'../README.md'
In [3]:
fobj.readline()
Out[3]:
'PyLunch Nova <sup>*</sup>\n'
In [4]:
print(fobj.tell())
fobj.readline()
print(fobj.tell())
26
52

Why do we care about objects?

Everything in Python is an object. (Even the classes themselves! Mind-bending...)

Objects hide details you don't care about (E.g., where am I in the file), and provide a unified view of what you do.

Objects make it easier for you to extend someone else's work without (E.g., domain-specific coordinates in astropy, your own point spread function in photutils).

Some More Terminology

  • "method": A function that's attached ("bound") to an object.
  • "class": The "template" for an object (defines the methods and sometimes default data). By convention uses a CamelCase name like MyClassName.
  • "instance": One particular object of a class (E.g. "that object is an instance of class File")
  • "instantiate/initialize": Creating a new instance of a class
  • __init__: A special name Python uses for the method called when a new object is instantiated
  • "subclass": A class that has everything some other class has, plus more (for "inheritance")

Some Examples (For you to try!)

Go to https://github.com/spacetelescope/pylunch and clone it (if you know git). Then just find the Object Oriented Intro.ipynb file.

Or

Go to the same page and click on: session2 -> Object Oriented Intro.ipynb. Then right click on the "Raw" button (upper right), and "save link as"/"download link" (or whatever is similar for your browser). Or just use this link (its the same thing).

Then

Once you've got it locally, open a terminal, cd into that directory, and run the command jupyter notebook.

Point/Circle Example

A point in 2D space has two coordinates. A circle can be thought of as a point with a radius. This is a common use of inheritance: we'll need to define a Point class to store the x and y positions, and a Circle class (which is a subclass of Point), which also has a radius.

Along the way we'll also implement methods to draw the objects (using the matplotlib plotting package). Don't worry about the details of anything that starts with plt, just know that it's calling matploltlib to actually make the plots.

In [ ]:
# This cell has some stuff needed to make the "draw" methods work.  It's not 
# important to understand them for now, but you do need to run them to  get
# things to work in the notebook

# This makes notebooks render plots inside the notebook
%matplotlib inline

# And these are imports from the "matplotlib" plotting package (more on that in a future session)
from matplotlib import pyplot as plt
In [ ]:
class Point:
    def __init__(self, x, y):
        # this is the method that gets called when you create a new Point
        self.x = x
        self.y = y
        
    def draw(self):
        plt.scatter([self.x], [self.y])

    
# if you're on python 2.x, the top line should be:
#class Point(object):
In [ ]:
p = Point(1, 2)
p.draw()  # You can just call draw, and not worry at all about *how* the drawing happens
In [ ]:
class Circle(Point):  # This means "define a class Circle that is a subclass of Point"
    def __init__(self, x, y, radius):
        super().__init__(x,y)
        # can also be this (works in py 2.x):
        #Point.__init__(self, x, y)
        self.radius = radius
    
    def draw(self): # this method "overrides" the `draw` method of Point
        super(Circle, self).draw()
        # can also be this (works in py 2.x):
        #Point.draw(self)
        # note for people not familiar with matplotlib: alpha keyword below sets transparency
        circle_patch = plt.Circle((self.x, self.y), self.radius, alpha=.5)
        plt.gca().add_patch(circle_patch)
        
    def compute_area(self):
        from math import pi
        return pi * self.radius**2
In [ ]:
pointishes = [Point(0.5, 2), Circle(2, 3, 1.2), Point(3.5, 4)]
for obj in pointishes:
    obj.draw() # again, all I need to know is that they "can be drawn"
print(pointishes[1].compute_area())

"Astronomer" Example

This is a more complex class heirarchy, which includes demonstrating multiple inheritance by way of the "diamond" pattern illustrated below. Also a fairly cynical view of how our science gets done.

<img src="AstronomerClassHeirarchy.svg" width=40%>

In [ ]:
class Astronomer:
    def __init__(self):
        self._science_done = []
        
    def do_science(self):
        raise NotImplementedError("What, you think astronomers are all alike? Pick something more specific.")
        
    @property
    def cv(self):
        return '\n'.join(self._science_done)
In [ ]:
class Observer(Astronomer):
    def __init__(self, favorite_targets):
        self.favorite_targets= favorite_targets
        super().__init__()
        
    def do_science(self):
        target, success = self.observe()
        
        if success:
            self._science_done.append('Sucessfully observed ' + target)
        else:
            self._science_done.append('Failed to observe ' + target)
    
    success_rate = .1  # TAC+weather if you're ground based, maybe?
    def observe(self):
        import random
        
        target = self.favorite_targets[random.randint(0, len(self.favorite_targets)-1)]        
        success = random.random() < self.success_rate
        
        return target, success
In [ ]:
class Theorist(Astronomer):
    def do_science(self):
        theory = self.make_theory()
        self._science_done.append('Foundational work on theory of ' + theory)
        
    def make_theory(self):
        import random
        # Eh, we just make it all up anyway
        theory = chr(random.randint(97, 97+25)).upper()
        for _ in range(random.randint(4, 10)):
            theory += chr(random.randint(97, 97+25))
            
        return theory
In [ ]:
class Hybrid(Observer, Theorist):
    def __init__(self, fraction_theorist, favorite_targets):
        super().__init__(favorite_targets)
        self.last_theory = None
        self.fraction_theorist = fraction_theorist
        
    def do_science(self):
        import random
        
        if self.last_theory is None or (random.random() < self.fraction_theorist):
            self.last_theory = self.make_theory()
        else:
            target, success = self.observe()
            if success:
                self._science_done.append('Used {} to prove theory {}'.format(target, self.last_theory))
            else:
                self._science_done.append('Used {} to falsify theory {}'.format(target, self.last_theory))
In [ ]:
iva_momcheva = Observer(['AEGIS', 'COSMOS', 'GOODS', 'UDS', 'Clg J0218.3-0510', 'assorted fancy lens clusters'])
erik_tollerud = Hybrid(.4, ['an M31 satellite', 'a Milky Way satellite', 'an isolated LG Dwarf', 'a Local Volume Dwarf'])

for _ in range(20):
    iva_momcheva.do_science()
    erik_tollerud.do_science()
    
print("Erik's CV:")
print(erik_tollerud.cv)

print("\nIva's CV:")
print(iva_momcheva.cv)
In [ ]:
fritz_zwicky = Theorist()

for _ in range(150):
    fritz_zwicky.do_science()

print("Zwicky's CV:")
print(fritz_zwicky.cv)

Now some ideas to play around with:

  • Extend the Point/Circle heirarchy to include Ellipse, or maybe Square (requires some knowledge of matplotlib)
  • Extend the Astronomer heirarchy. Maybe specific kinds of observers?
  • Make your own "toy" class heirarchy from scratch. Make sure to actually instantitate some objects and test that they work
  • Chat with someone about or hack on a class heirarchy for some of your own data reduction/analysis code.
  • Try more complicated multiple inheritance heirarchies, and try to infer how super() works (you'll probably have some surprises!).
  • If you think you already understand classes and want to blow your mind: Experiment with metaclasses (E.g., https://docs.python.org/3/reference/datamodel.html#customizing-class-creation)